<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go OTel 应用逻辑解释 - 初始化设置</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
<h1>初始化设置</h1>

<nav>
    <ul>
        <li><a href="index.html">概述</a></li>
        <li><a href="setup.html" class="active">初始化设置</a></li>
        <li><a href="handlers.html">HTTP 处理器</a></li>
        <li><a href="services.html">业务逻辑服务</a></li>
        <li><a href="main_flow.html">主程序流程</a></li>
        <li><a href="glossary.html">名词解释</a></li>
    </ul>
</nav>

<section>
    <h2>引言</h2>
    <p>
        在应用程序处理任何请求之前，我们需要正确地设置和初始化所有的基础组件，包括 OpenTelemetry 追踪系统、数据库连接 (MySQL/GORM) 和 Redis 缓存连接。这些初始化步骤通常在 <code>main</code> 函数的早期阶段完成。
    </p>
</section>

<section id="otel-init">
    <h2>1. OpenTelemetry 初始化 (<code>initTracerProvider</code>)</h2>
    <p>
        这是配置分布式追踪系统的核心步骤。目标是创建一个全局的 <a href="glossary.html#tracer-provider"><code>TracerProvider</code></a>，它负责管理和导出追踪数据。
    </p>
    <pre><code class="language-go">// initTracerProvider 初始化 OpenTelemetry SDK TracerProvider。
func initTracerProvider() (func(context.Context) error, error) {
    <span class="code-comment">// 1. 创建 Exporter: 定义追踪数据发送到哪里 (这里是标准输出)</span>
    exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
    // ... error handling ...

    <span class="code-comment">// 2. 创建 Resource: 描述服务信息 (服务名、版本等)</span>
    res, err := resource.Merge(
        resource.Default(), <span class="code-comment">// 获取 Go 版本、OS 等默认信息 + SDK 对应的 Schema URL</span>
        resource.NewSchemaless( <span class="code-comment">// 添加自定义属性，不带 Schema URL</span>
            semconv.ServiceName("gin-gorm-mysql-redis-service"),
            semconv.ServiceVersion("v1.4.3"),
            attribute.String("environment", "development"),
        ),
    )
    // ... error handling ...

    <span class="code-comment">// 3. 创建 TracerProvider: 配置采样、批处理和资源</span>
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithSampler(sdktrace.AlwaysSample()), <span class="code-comment">// 对所有追踪进行采样</span>
        sdktrace.WithBatcher(exporter),               <span class="code-comment">// 批量导出 Span</span>
        sdktrace.WithResource(res),                  <span class="code-comment">// 关联服务资源信息</span>
    )

    <span class="code-comment">// 4. 设置为全局 Provider</span>
    otel.SetTracerProvider(tp)

    <span class="code-comment">// 5. 设置全局 Propagator: 定义追踪上下文如何跨进程传播 (例如通过 HTTP Headers)</span>
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{}, <span class="code-comment">// W3C 标准</span>
        propagation.Baggage{},
    ))

    <span class="code-comment">// 返回一个关闭函数，用于程序退出时清理资源</span>
    return tp.Shutdown, nil
}</code></pre>
    <h3>逻辑解释:</h3>
    <ul>
        <li><strong>Exporter (导出器):</strong> 决定追踪数据 (<a href="glossary.html#span">Spans</a>) 被发送到哪里。代码中使用了 <code>stdouttrace</code>，它会将追踪信息打印到控制台，非常适合开发和调试。在生产环境中，通常会替换为 OTLP Exporter，将数据发送到像 Jaeger 或 Tempo 这样的追踪后端系统。</li>
        <li><strong>Resource (资源):</strong> 定义了产生这些追踪数据的服务的基本信息。<code>resource.Default()</code> 会自动添加一些有用的默认信息（比如 Go 运行时版本），并包含与当前 OTel SDK 版本兼容的 <a href="glossary.html#schema-url">Schema URL</a>。<code>resource.NewSchemaless(...)</code> 用于添加我们自己的属性，比如服务名、版本号和环境，而不会引入 Schema URL 冲突。<code>resource.Merge()</code> 将两者合并。这些信息会附加到从这个服务发出的所有 Span 上。</li>
        <li><strong>TracerProvider (追踪提供者):</strong> 这是 OTel SDK 的核心。它使用配置好的 Exporter 和 Resource。<code>WithSampler</code> 设置采样策略（这里是全部采样）。<code>WithBatcher</code> 使用 Exporter 来批量处理 Span，提高效率。</li>
        <li><strong>全局设置:</strong> 通过 <code>otel.SetTracerProvider</code> 和 <code>otel.SetTextMapPropagator</code> 将配置好的 Provider 和 <a href="glossary.html#propagator">Propagator</a> 注册为全局实例。这样，应用的其他部分可以通过 <code>otel.Tracer(...)</code> 获取 Tracer，并且像 <a href="glossary.html#otelgin"><code>otelgin</code></a> 这样的中间件就能自动使用正确的配置来处理追踪上下文的传播。</li>
        <li><strong>Shutdown 函数:</strong> 返回的函数用于在程序结束时调用，确保所有缓存的 Span 都被成功导出，避免数据丢失。</li>
    </ul>
</section>

<section id="db-init">
    <h2>2. 数据库初始化 (<code>initDB</code>)</h2>
    <p>
        这个函数负责连接到 MySQL 数据库，使用 <a href="glossary.html#gorm">GORM</a> 作为 <a href="glossary.html#orm">ORM</a>，并集成 GORM 的 OTel 插桩。
    </p>
    <pre><code class="language-go">// initDB 初始化 GORM 并连接到 MySQL 数据库。
func initDB() (*gorm.DB, error) {
    <span class="code-comment">// 获取数据库连接字符串 (DSN)</span>
    dsn := os.Getenv("MYSQL_DSN")
    // ... provide default DSN if env var is empty ...

    log.Println("正在连接到 MySQL 数据库...")
    gormConfig := &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info), <span class="code-comment">// 配置 GORM 日志级别</span>
    }
    database, err := gorm.Open(mysql.Open(dsn), gormConfig)

    <span class="code-comment">// --- 尝试自动创建数据库 (如果连接失败) ---</span>
    if err != nil {
        log.Printf("连接数据库失败..., 尝试自动创建...")
        // ... (省略了连接 root 用户并执行 CREATE DATABASE 的逻辑) ...
        database, err = gorm.Open(mysql.Open(dsn), gormConfig) // 重新连接
        // ... error handling ...
    }
    log.Println("成功连接到 MySQL 数据库 'otel_demo'。")

    <span class="code-comment">// --- 集成 OTel GORM 插件 ---</span>
    log.Println("正在集成 OTel GORM 插件...")
    if err := database.Use(otelgorm.NewPlugin( <span class="code-comment">// 使用 otelgorm 插件</span>
        otelgorm.WithAttributes(semconv.DBSystemMySQL), <span class="code-comment">// 添加标准数据库属性</span>
        otelgorm.WithDBName("otel_demo"),             <span class="code-comment">// 指定数据库名</span>
    )); err != nil {
        log.Printf("警告: 集成 otelgorm 插件失败: %v", err)
    } else {
        log.Println("OTel GORM 插件集成成功。")
    }

    <span class="code-comment">// --- 数据库迁移 ---</span>
    log.Println("正在执行数据库自动迁移 (AutoMigrate)...")
    err = database.AutoMigrate(&User{}) <span class="code-comment">// 根据 User struct 创建或更新表结构</span>
    // ... error handling ...

    return database, nil
}</code></pre>
    <h3>逻辑解释:</h3>
    <ul>
        <li><strong>连接数据库:</strong> 使用提供的 <a href="glossary.html#dsn">DSN</a> (数据源名称) 和 GORM 的 MySQL 驱动尝试连接数据库。同时配置了 GORM 的日志记录器，设为 <code>Info</code> 级别可以在控制台看到执行的 SQL 语句。</li>
        <li><strong>自动创建数据库:</strong> 如果初始连接失败（比如数据库 `otel_demo` 还不存在），代码会尝试用一个更高权限的 DSN（不指定数据库名）连接到 MySQL 服务器，执行 <code>CREATE DATABASE IF NOT EXISTS otel_demo</code> 语句，然后再次尝试用原始 DSN 连接。这提高了首次运行时的便利性。</li>
        <li><strong>集成 <code>otelgorm</code>:</strong> 这是关键的 OTel 集成点。通过 <code>database.Use(otelgorm.NewPlugin(...))</code>，我们将 <a href="glossary.html#otelgorm"><code>otelgorm</code></a> 插件注册到 GORM 实例。此后，所有通过这个 <code>database</code> 实例执行的 GORM 操作（如 <code>Create</code>, <code>First</code>, <code>Update</code>, <code>Delete</code>）都会被 <span class="highlight">自动地</span> 创建一个相应的 OTel Span，并附带数据库相关的属性（如 SQL 语句、操作类型）。</li>
        <li><strong>数据库迁移:</strong> <code>database.AutoMigrate(&User{})</code> 是 GORM 的一个便利功能。它会检查 Go 代码中定义的 <code>User</code> 结构体，并与数据库中的 <code>users</code> 表进行比较。如果表不存在，它会创建表；如果表存在但结构不匹配（比如少了个字段），它会尝试修改表结构（添加列等，但通常不删除列）。</li>
    </ul>
</section>

<section id="redis-init">
    <h2>3. Redis 初始化 (<code>initRedis</code>)</h2>
    <p>
        此函数负责连接到 Redis 服务器，并集成 Redis 的 OTel 插桩。
    </p>
    <pre><code class="language-go">// initRedis 初始化 Redis 客户端连接并集成 OTel (redisotel)。
func initRedis() (*redis.Client, error) {
    <span class="code-comment">// 获取 Redis 地址和密码</span>
    redisAddr := os.Getenv("REDIS_ADDR")
    // ... provide default address if env var is empty ...
    redisPassword := os.Getenv("REDIS_PASSWORD")

    log.Printf("正在连接到 Redis (地址: %s)...", redisAddr)
    rdbClient := redis.NewClient(&redis.Options{ <span class="code-comment">// 创建 Redis 客户端实例</span>
        Addr:     redisAddr,
        Password: redisPassword,
        DB:       0, <span class="code-comment">// 使用默认数据库 0</span>
    })

    <span class="code-comment">// --- 集成 OTel Redis 插件 (redisotel) ---</span>
    log.Println("正在集成 OTel Redis 插件 (redisotel/v9)...")
    if err := redisotel.InstrumentTracing(rdbClient); err != nil { <span class="code-comment">// **关键:** 包装客户端</span>
        log.Printf("警告: 集成 redisotel 插件失败: %v", err)
    } else {
        log.Println("OTel Redis 插件 (redisotel) 集成成功。")
    }

    <span class="code-comment">// --- 测试连接 ---</span>
    log.Println("正在测试 Redis 连接 (PING)...")
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    _, err := rdbClient.Ping(ctx).Result() <span class="code-comment">// 执行 PING 命令检查连接</span>
    // ... error handling ...
    log.Println("成功连接到 Redis 并收到 PONG。")

    return rdbClient, nil
}</code></pre>
    <h3>逻辑解释:</h3>
    <ul>
        <li><strong>创建客户端:</strong> 使用 <code>github.com/redis/go-redis/v9</code> 库创建一个 Redis 客户端实例，配置服务器地址、密码（如果有）和数据库编号。</li>
        <li><strong>集成 <code>redisotel</code>:</strong> 这是另一个关键的 OTel 集成点。通过调用 <a href="glossary.html#redisotel"><code>redisotel.InstrumentTracing(rdbClient)</code></a>，我们 <span class="highlight">修改（包装）</span> 了原始的 Redis 客户端。从此以后，通过这个 <code>rdbClient</code> 实例执行的所有 Redis 命令（如 <code>Get</code>, <code>Set</code>, <code>Del</code>, <code>Ping</code> 等）都会被 <span class="highlight">自动地</span> 创建一个相应的 OTel Span，并包含 Redis 命令、Key 等相关属性。</li>
        <li><strong>测试连接:</strong> 调用 <code>rdbClient.Ping(ctx).Result()</code> 向 Redis 服务器发送 PING 命令。如果服务器正常响应 PONG，则表示连接成功。这个 PING 操作本身也会因为 <code>redisotel</code> 的集成而被追踪。</li>
    </ul>
</section>

</body>
</html>